/*
 * Copyright 2008 M-Way Solutions
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.mwaysolutions.ocm.impl;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.jcr.NamespaceRegistry;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Workspace;
import javax.jcr.nodetype.NoSuchNodeTypeException;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException;
import org.apache.jackrabbit.core.nodetype.NodeDefImpl;
import org.apache.jackrabbit.core.nodetype.NodeTypeDef;
import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.nodetype.PropDefImpl;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.NameFactory;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;

import com.mwaysolutions.ocm.OcmProperty;
import com.mwaysolutions.ocm.OcmPropertyDescriptor;
import com.mwaysolutions.ocm.OcmRegister;
import com.mwaysolutions.ocm.OcmType;
import com.mwaysolutions.ocm.OcmTypeDescriptor;
import com.mwaysolutions.ocm.OcmTypeRegister;
import com.mwaysolutions.ocm.OcmUtils;
import com.mwaysolutions.ocm.OcmWriter;
import com.mwaysolutions.ocm.Optional;

/**
 *
 * Data types (TODO unmaintained incomplete proposal):
 * <ul>
 * <li>primitive, wrapper, stream, class reference = property</li>
 * <li>arrays of primitives, wrappers, streams, class references = array
 * poroperty</li>
 * <li>class = node</li>
 *
 * <li>array of classes = wrapper node (type=array) / item</li>
 *
 * <li>collection of primitives, wrappers, streams, class references = wrapper
 * node (type=xyzCollection) / property</li>
 * <li>collection of classes = wrapper node (type=xyzCollection) / item</li>
 *
 * <li>collection of primitives, wrappers, streams, class references = wrapper
 * node (type=xyzCollection) / property</li>
 * <li>collection of classes = wrapper node (type=xyzCollection) / item</li>
 *
 * </ul>
 *
 * Note that null values in arrays/collections/maps is not supported due to
 * nature of JCR.
 *
 * @author <a href="mailto:m.zdila@mwaysolutions.com">Martin Ždila</a>
 *
 */
public class OcmRegisterImpl implements OcmRegister {

	private final OcmTypeRegister ocmTypeRegister;
	private final Log log = LogFactory.getLog(OcmRegisterImpl.class);
	private final NameFactory nameFactory = NameFactoryImpl.getInstance();


	public OcmRegisterImpl(final OcmTypeRegister ocmTypeRegister) {
		this.ocmTypeRegister = ocmTypeRegister;
	}


	@Override
	public void registerType(final Workspace workspace, final Class<?> clazz, final boolean reregister)
			throws RepositoryException {

		final OcmType ocmType = (clazz.isArray() ? clazz.getComponentType() : clazz).getAnnotation(OcmType.class);
		if (ocmType == null) {
			throw new IllegalArgumentException(clazz.getName() + " is not an OcmType");
		}

		registerType(workspace, clazz, ocmType.name() + (clazz.isArray() ? "_Array" : ""), reregister);
	}


	@Override
	public void registerType(final Workspace workspace, final Class<?> clazz, final String name, final boolean reregister)
		throws RepositoryException {

		log.debug("Registering type " + clazz.getCanonicalName() + " with name " + name);


		final OcmTypeDescriptor ocmTypeDescriptor = new OcmTypeDescriptor(name, clazz);
		ocmTypeRegister.register(ocmTypeDescriptor);

		boolean register = reregister;

		try {
			workspace.getNodeTypeManager().getNodeType(name);
			// already registered in JCR
		} catch (final NoSuchNodeTypeException e) {
			register = true;
			// this is ok, we will register new type
		}

		final NamespaceRegistry nsreg = workspace.getNamespaceRegistry();
		final NodeTypeDef ntd = new NodeTypeDef();

		final Name nodeName;
		if (register) {
			nodeName = createName(nameFactory, nsreg, name);
			ntd.setName(nodeName);
		} else {
			nodeName = null;
		}

		// TODO:ntd.setOrderableChildNodes(arg0)

		final List<Name> superTypeList = new ArrayList<Name>();

		// make it subclass (sub-type) if immediately extends class annotated with OcmType
		final Class<?> superClass = clazz.getSuperclass();
		final OcmTypeDescriptor superOcmTypeDescriptor = superClass == null ? null : ocmTypeRegister.getTypeDescriptorByClass(superClass, false);
		if (superOcmTypeDescriptor != null) {
			if (register) {
				superTypeList.add(createName(nameFactory, nsreg, superOcmTypeDescriptor.getNodeTypeName()));
			}
			ocmTypeDescriptor.inheritProperties(superOcmTypeDescriptor);
		} else {
			if (register) {
//				superTypeList.add(new QName(nsreg.getURI("nt"), "base"));
				superTypeList.add(nameFactory.create(nsreg.getURI("nt"), "unstructured"));	// TODO: unstructured is temporary solution
			}
		}

		// add mixins for interfaces
		for (final Class<?> iface : clazz.getInterfaces()) {
			final OcmTypeDescriptor ifaceOcmTypeDescriptor = ocmTypeRegister.getTypeDescriptorByClass(iface, false);
			if (ifaceOcmTypeDescriptor == null) {
				continue;
			}

			if (register) {
				superTypeList.add(createName(nameFactory, nsreg, ifaceOcmTypeDescriptor.getNodeTypeName()));
			}
			ocmTypeDescriptor.inheritProperties(ifaceOcmTypeDescriptor);
		}


		final List<PropDefImpl> propDefs = new ArrayList<PropDefImpl>();
		final List<NodeDefImpl> nodeDefs = new ArrayList<NodeDefImpl>();
//		final Set<String> propertySet = new HashSet<String>(); // set for checking if there are not duplicated property definitions
		final String propertyNamespace = StringUtils.substringBefore(name, ":");


		if (register) {
			if (clazz.isArray() || clazz == List.class) {
				final OcmTypeDescriptor valueTypeDescriptor = ocmTypeRegister.getTypeDescriptorByClass(
						clazz.isArray() ? clazz.getComponentType() : clazz.getTypeParameters()[0].getGenericDeclaration(), true);
				final Name typeName = createName(nameFactory, nsreg, valueTypeDescriptor.getNodeTypeName());
				final NodeDefImpl nodeDef = new NodeDefImpl();
				nodeDef.setName(createName(nameFactory, nsreg, OcmWriter.COLLECTION_ITEM_NAME));
				nodeDef.setAllowsSameNameSiblings(true);
				nodeDef.setDeclaringNodeType(nodeName);
				nodeDef.setDefaultPrimaryType(typeName);
				nodeDef.setRequiredPrimaryTypes(new Name[] { typeName });
				nodeDef.setMandatory(false);
				nodeDef.setProtected(false);
				nodeDef.setAutoCreated(false);
				nodeDefs.add(nodeDef);
			} else if (clazz == Map.class) {
				// leave unstructured
			}
		}


		for (final Method method : clazz.getDeclaredMethods()) {
			final OcmProperty ocmProperty = method.getAnnotation(OcmProperty.class);
			if (ocmProperty == null) {
				continue;
			}

			final String beanPropertyName = OcmPropertyUtils.getPropertyName(method.getName());
			final String jcrPropertyName = StringUtils.defaultIfEmpty(ocmProperty.name(), propertyNamespace + ':' + beanPropertyName);

//			boolean unique = true;
//			if (ocmProperty.partial()) {
//				unique &= propertySet.add((OcmUtils.isSetter(method) ? "set-" : "get-") + jcrPropertyName);
//				if (propertySet.contains((OcmUtils.isSetter(method) ? "get-" : "set-") + jcrPropertyName)) {
//					continue; // already registered property
//				}
//			} else {
//				unique &= propertySet.add("set-" + jcrPropertyName);
//				unique &= propertySet.add("get-" + jcrPropertyName);
//			}
//
//			if (!unique) {
//				throw new IllegalArgumentException("JCR Property '" + jcrPropertyName + "' is duplicated in the bean");
//			}

			ocmTypeDescriptor.addProperty(new OcmPropertyDescriptor(beanPropertyName, jcrPropertyName, ocmProperty.reference(), ocmProperty.unique()));

			if (!register) {
				continue;
			}

			final Class<?> type = method.getReturnType();

			if (type == Object.class) {
				continue; // leave nt:unstructured only
			}

			final OcmTypeDescriptor valueTypeDescriptor = ocmTypeRegister.getTypeDescriptorByClass(type, false);

			final boolean mandatory = !method.isAnnotationPresent(Optional.class);

			if (valueTypeDescriptor != null) { // registered: class / array of classes / collection / map
				if (ocmProperty.reference()) {
					if (type.isArray() || Collection.class.isAssignableFrom(type)) {
						final PropDefImpl propDef = new PropDefImpl();
						propDef.setName(createName(nameFactory, nsreg, jcrPropertyName));
						propDef.setRequiredType(PropertyType.REFERENCE);
						propDef.setDeclaringNodeType(nodeName);
						propDef.setMandatory(mandatory);
						propDef.setMultiple(true);
						propDef.setProtected(ocmProperty.writeProtected());
						propDef.setAutoCreated(ocmProperty.autocreated());
						propDefs.add(propDef);
					} else if (Map.class.isAssignableFrom(type)) {
						// leave unstructured
					} else { // simple bean
						final PropDefImpl propDef = new PropDefImpl();
						propDef.setName(createName(nameFactory, nsreg, jcrPropertyName));
						propDef.setRequiredType(PropertyType.REFERENCE);
						propDef.setDeclaringNodeType(nodeName);
						propDef.setMandatory(mandatory);
						propDef.setMultiple(false);
						propDef.setProtected(ocmProperty.writeProtected());
						propDef.setAutoCreated(ocmProperty.autocreated());
						propDefs.add(propDef);
					}
				} else {
					final Name typeName = createName(nameFactory, nsreg, valueTypeDescriptor.getNodeTypeName());
					final NodeDefImpl nodeDef = new NodeDefImpl();
					nodeDef.setName(createName(nameFactory, nsreg, jcrPropertyName));
					nodeDef.setAllowsSameNameSiblings(false);
					nodeDef.setDeclaringNodeType(nodeName);
					nodeDef.setDefaultPrimaryType(typeName);
					nodeDef.setRequiredPrimaryTypes(new Name[] { typeName });
					nodeDef.setMandatory(mandatory);
					nodeDef.setProtected(ocmProperty.writeProtected());
					nodeDef.setAutoCreated(ocmProperty.autocreated());
					nodeDefs.add(nodeDef);
				}
			} else if (type.isArray() && !byte[].class.isAssignableFrom(type)) { // should be primitive array (excluding byte array)
				final PropDefImpl propDef = new PropDefImpl();
				propDef.setName(createName(nameFactory, nsreg, jcrPropertyName));
				propDef.setRequiredType(OcmUtils.getJcrPropertyType(type.getComponentType()));
				propDef.setDeclaringNodeType(nodeName);
				propDef.setMandatory(mandatory);
				propDef.setMultiple(true);
				propDef.setProtected(ocmProperty.writeProtected());
				propDef.setAutoCreated(ocmProperty.autocreated());
				propDefs.add(propDef);
			} else if (!jcrPropertyName.equals("jcr:uuid")) { // should be jcr-primitive
				final PropDefImpl propDef = new PropDefImpl();
				propDef.setName(createName(nameFactory, nsreg, jcrPropertyName));
				propDef.setRequiredType(OcmUtils.getJcrPropertyType(type));
				propDef.setDeclaringNodeType(nodeName);
				propDef.setMandatory(mandatory);
				propDef.setMultiple(false);
				propDef.setProtected(ocmProperty.writeProtected());
				propDef.setAutoCreated(ocmProperty.autocreated());
				propDefs.add(propDef);
			}
		}


		if (register) {
			ntd.setSupertypes(superTypeList.toArray(new Name[0]));
			ntd.setMixin(clazz.isInterface());
			ntd.setPropertyDefs(propDefs.toArray(new PropDefImpl[propDefs.size()]));
			ntd.setChildNodeDefs(nodeDefs.toArray(new NodeDefImpl[nodeDefs.size()]));

			final NodeTypeRegistry nodeTypeRegistry = ((NodeTypeManagerImpl) workspace.getNodeTypeManager()).getNodeTypeRegistry();
			try {
				if (reregister) {
					nodeTypeRegistry.reregisterNodeType(ntd);
				} else {
					nodeTypeRegistry.registerNodeType(ntd);
				}
			} catch (final InvalidNodeTypeDefException e) {
				throw new RepositoryException(e);
			}
		}
	}

	
	private static Name createName(final NameFactory nameFactory, final NamespaceRegistry nsreg, final String qnameStr) throws RepositoryException {
		final String[] qname = StringUtils.split(qnameStr, ':');
		return nameFactory.create(nsreg.getURI(qname[0]), qname[1]);
	}

}
